// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.psi.impl.source.xml;

import com.intellij.javaee.ExternalResourceManager;
import com.intellij.javaee.ExternalResourceManagerEx;
import com.intellij.lang.ASTNode;
import com.intellij.lang.dtd.DTDLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.PsiCachedValueImpl;
import com.intellij.psi.impl.meta.MetaRegistry;
import com.intellij.psi.impl.source.html.dtd.HtmlNSDescriptorImpl;
import com.intellij.psi.impl.source.tree.CompositePsiElement;
import com.intellij.psi.meta.PsiMetaData;
import com.intellij.psi.meta.PsiMetaOwner;
import com.intellij.psi.tree.ChildRoleBase;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.CachedValue;
import com.intellij.psi.util.CachedValueProvider;
import com.intellij.psi.util.CachedValuesManager;
import com.intellij.psi.xml.*;
import com.intellij.util.AstLoadingFilter;
import com.intellij.util.concurrency.AtomicFieldUpdater;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.Html5SchemaProvider;
import com.intellij.xml.XmlExtension;
import com.intellij.xml.XmlNSDescriptor;
import com.intellij.xml.index.XmlNamespaceIndex;
import com.intellij.xml.util.XmlNSDescriptorSequence;
import com.intellij.xml.util.XmlPsiUtil;
import com.intellij.xml.util.XmlUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class XmlDocumentImpl extends XmlElementImpl implements XmlDocument {

  private static final Key<Boolean> AUTO_GENERATED = Key.create("auto-generated xml schema");

  public static boolean isAutoGeneratedSchema(XmlFile file) {
    return file.getUserData(AUTO_GENERATED) != null;
  }

  private static final Logger LOG = Logger.getInstance(XmlDocumentImpl.class);
  private static final AtomicFieldUpdater<XmlDocumentImpl, XmlProlog>
    MY_PROLOG_UPDATER = AtomicFieldUpdater.forFieldOfType(XmlDocumentImpl.class, XmlProlog.class);
  private static final AtomicFieldUpdater<XmlDocumentImpl, XmlTag>
    MY_ROOT_TAG_UPDATER = AtomicFieldUpdater.forFieldOfType(XmlDocumentImpl.class, XmlTag.class);

  private volatile XmlProlog myProlog;
  private volatile XmlTag myRootTag;
  private volatile long myExtResourcesModCount = -1;

  public XmlDocumentImpl() {
    this(XmlElementType.XML_DOCUMENT);
  }

  protected XmlDocumentImpl(IElementType type) {
    super(type);
  }

  @Override
  public void accept(@NotNull PsiElementVisitor visitor) {
    if (visitor instanceof XmlElementVisitor) {
      ((XmlElementVisitor)visitor).visitXmlDocument(this);
    }
    else {
      visitor.visitElement(this);
    }
  }

  @Override
  public int getChildRole(@NotNull ASTNode child) {
    LOG.assertTrue(child.getTreeParent() == this);
    IElementType i = child.getElementType();
    if (i == XmlElementType.XML_PROLOG) {
      return XmlChildRole.XML_PROLOG;
    }
    else if (i instanceof IXmlTagElementType) {
      return XmlChildRole.XML_TAG;
    }
    else {
      return ChildRoleBase.NONE;
    }
  }

  @Override
  public XmlProlog getProlog() {
    XmlProlog prolog = myProlog;

    if (prolog == null) {
      prolog = (XmlProlog)findElementByTokenType(XmlElementType.XML_PROLOG);

      if(!MY_PROLOG_UPDATER.compareAndSet(this, null, prolog)) {
        prolog = MY_PROLOG_UPDATER.get(this);
      }
    }

    return prolog;
  }

  @Override
  public XmlTag getRootTag() {
    XmlTag rootTag = myRootTag;

    if (rootTag == null) {
      rootTag = (XmlTag)XmlPsiUtil.findElement(this, IXmlTagElementType.class::isInstance);

      if (!MY_ROOT_TAG_UPDATER.compareAndSet(this, null, rootTag)) {
        rootTag = MY_ROOT_TAG_UPDATER.get(this);
      }
    }

    return rootTag;
  }

  @Override
  public XmlNSDescriptor getRootTagNSDescriptor() {
    XmlTag rootTag = getRootTag();
    return rootTag != null ? rootTag.getNSDescriptor(rootTag.getNamespace(), false) : null;
  }

  private ConcurrentMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheStrict =
    ContainerUtil.newConcurrentMap();
  private ConcurrentMap<String, CachedValue<XmlNSDescriptor>> myDefaultDescriptorsCacheNotStrict =
    ContainerUtil.newConcurrentMap();

  @Override
  public void clearCaches() {
    myDefaultDescriptorsCacheStrict.clear();
    myDefaultDescriptorsCacheNotStrict.clear();
    myRootTag = null;
    myProlog = null;
    super.clearCaches();
  }

  @Nullable
  @Override
  public XmlNSDescriptor getDefaultNSDescriptor(final String namespace, final boolean strict) {
    long curExtResourcesModCount = ExternalResourceManagerEx.getInstanceEx().getModificationCount(getProject());
    if (myExtResourcesModCount != curExtResourcesModCount) {
      myDefaultDescriptorsCacheNotStrict.clear();
      myDefaultDescriptorsCacheStrict.clear();
      myExtResourcesModCount = curExtResourcesModCount;
    }

    final ConcurrentMap<String, CachedValue<XmlNSDescriptor>> defaultDescriptorsCache;
    if (strict) {
      defaultDescriptorsCache = myDefaultDescriptorsCacheStrict;
    }
    else {
      defaultDescriptorsCache = myDefaultDescriptorsCacheNotStrict;
    }

    CachedValue<XmlNSDescriptor> cachedValue = defaultDescriptorsCache.get(namespace);
    if (cachedValue == null) {
      defaultDescriptorsCache.put(namespace, cachedValue = new PsiCachedValueImpl<>(getManager(), () -> {
        final XmlNSDescriptor defaultNSDescriptorInner = getDefaultNSDescriptorInner(namespace, strict);

        if (isGeneratedFromDtd(defaultNSDescriptorInner)) {
          return new CachedValueProvider.Result<>(defaultNSDescriptorInner, this, ExternalResourceManager.getInstance());
        }

        return new CachedValueProvider.Result<>(defaultNSDescriptorInner, defaultNSDescriptorInner != null
                                                                          ? defaultNSDescriptorInner.getDependencies()
                                                                          : ExternalResourceManager.getInstance());
      }));
    }
    return cachedValue.getValue();
  }

  private boolean isGeneratedFromDtd(XmlNSDescriptor defaultNSDescriptorInner) {
    if (defaultNSDescriptorInner == null) {
      return false;
    }
    XmlFile descriptorFile = defaultNSDescriptorInner.getDescriptorFile();
    if (descriptorFile == null) {
        return false;
    }
    @NonNls String otherName = XmlUtil.getContainingFile(this).getName() + ".dtd";
    return descriptorFile.getName().equals(otherName);
  }

  private XmlNSDescriptor getDefaultNSDescriptorInner(final String namespace, final boolean strict) {
    final XmlFile containingFile = XmlUtil.getContainingFile(this);
    if (containingFile == null) return null;
    final XmlProlog prolog = getProlog();
    final XmlDoctype doctype = prolog != null ? prolog.getDoctype() : null;
    boolean dtdUriFromDocTypeIsNamespace = false;

    if (XmlUtil.HTML_URI.equals(namespace)) {
      XmlNSDescriptor nsDescriptor = doctype != null ? getNsDescriptorFormDocType(doctype, containingFile, true) : null;
      if (doctype != null) {
        LOG.debug(
          "Descriptor from doctype " + doctype + " is " + (nsDescriptor != null ? nsDescriptor.getClass().getCanonicalName() : "NULL"));
      }

      if (nsDescriptor == null) {
        String htmlns = ExternalResourceManagerEx.getInstanceEx().getDefaultHtmlDoctype(getProject());
        if (htmlns.isEmpty()) {
          htmlns = Html5SchemaProvider.getHtml5SchemaLocation();
        }
        nsDescriptor = getDefaultNSDescriptor(htmlns, false);
      }
      if (nsDescriptor != null) {
        final XmlFile descriptorFile = nsDescriptor.getDescriptorFile();
        if (descriptorFile != null) {
          return getCachedHtmlNsDescriptor(descriptorFile);
        }
      }
      return new HtmlNSDescriptorImpl(nsDescriptor);
    }
    else if (XmlUtil.XHTML_URI.equals(namespace)) {
      String xhtmlNamespace = XmlUtil.getDefaultXhtmlNamespace(getProject());
      if (xhtmlNamespace == null || xhtmlNamespace.isEmpty()) {
        xhtmlNamespace = Html5SchemaProvider.getXhtml5SchemaLocation();
      }
      return getDefaultNSDescriptor(xhtmlNamespace, false);
    }
    else if (namespace != null && namespace != XmlUtil.EMPTY_URI) {
      if (doctype == null || !namespace.equals(XmlUtil.getDtdUri(doctype))) {
        boolean documentIsSchemaThatDefinesNs = namespace.equals(XmlUtil.getTargetSchemaNsFromTag(getRootTag()));

        final XmlFile xmlFile = documentIsSchemaThatDefinesNs
                                ? containingFile
                                : XmlUtil.findNamespace(containingFile, namespace);
        if (xmlFile != null) {
          final XmlDocument document = AstLoadingFilter.forceAllowTreeLoading(xmlFile, () -> xmlFile.getDocument());
          if (document != null) {
            return (XmlNSDescriptor)document.getMetaData();
          }
        }
      } else {
        dtdUriFromDocTypeIsNamespace = true;
      }
    }

    if (strict && !dtdUriFromDocTypeIsNamespace) return null;

    if (doctype != null) {
      XmlNSDescriptor descr = getNsDescriptorFormDocType(doctype, containingFile, false);

      if (descr != null) {
        return XmlExtension.getExtension(containingFile).getDescriptorFromDoctype(containingFile, descr);
      }
    }

    if (strict) return null;
    if (namespace == XmlUtil.EMPTY_URI) {
      final XmlFile xmlFile = XmlUtil.findNamespace(containingFile, namespace);
      if (xmlFile != null) {
        return (XmlNSDescriptor)xmlFile.getDocument().getMetaData();
      }
    }
    try {
      final PsiFile fileFromText = PsiFileFactory.getInstance(getProject())
        .createFileFromText(containingFile.getName() + ".dtd", DTDLanguage.INSTANCE, XmlUtil.generateDocumentDTD(this, false), false, false);
      if (fileFromText instanceof XmlFile) {
        fileFromText.putUserData(AUTO_GENERATED, Boolean.TRUE);
        return (XmlNSDescriptor)((XmlFile)fileFromText).getDocument().getMetaData();
      }
    }
    catch (ProcessCanceledException ex) {
      throw ex;
    }
    catch (RuntimeException ignored) {
    } // e.g. dtd isn't mapped to xml type

    return null;
  }

  @Nullable
  public static XmlNSDescriptor getCachedHtmlNsDescriptor(@NotNull final XmlFile descriptorFile) {
    return getCachedHtmlNsDescriptor(descriptorFile, "");
  }

  @Nullable
  public static XmlNSDescriptor getCachedHtmlNsDescriptor(@NotNull final XmlFile descriptorFile, @NotNull final String prefix) {
    Map<String, XmlNSDescriptor> descriptorsByPrefix = CachedValuesManager.getCachedValue(descriptorFile, () -> {
      return CachedValueProvider.Result.create(new ConcurrentHashMap<>(), descriptorFile);
    });
    return descriptorsByPrefix.computeIfAbsent(prefix, p -> {
      final XmlDocument document = descriptorFile.getDocument();
      if (document == null) return null;
      return new HtmlNSDescriptorImpl((XmlNSDescriptor)document.getMetaData());
    });
  }

  @NotNull
  private static String getFilePathForLogging(@Nullable PsiFile file) {
    if (file == null) {
      return "NULL";
    }
    final VirtualFile vFile = file.getVirtualFile();
    return vFile != null ? vFile.getPath() : "NULL_VFILE";
  }

  @Nullable
  private XmlNSDescriptor getNsDescriptorFormDocType(final XmlDoctype doctype, final XmlFile containingFile, final boolean forHtml) {
    XmlNSDescriptor descriptor = getNSDescriptorFromMetaData(doctype.getMarkupDecl(), true);

    final String filePath = getFilePathForLogging(containingFile);

    final String dtdUri = XmlUtil.getDtdUri(doctype);
    LOG.debug("DTD url for doctype " + doctype.getText() + " in file " + filePath + " is " + dtdUri);

    if (dtdUri != null && !dtdUri.isEmpty()){
      XmlFile xmlFile = XmlUtil.findNamespace(containingFile, dtdUri);
      if (xmlFile == null) {
        // try to auto-detect it
        xmlFile = XmlNamespaceIndex.guessDtd(dtdUri, containingFile);
      }
      final String schemaFilePath = getFilePathForLogging(xmlFile);

      LOG.debug("Schema file for " + filePath + " is " + schemaFilePath);

      XmlNSDescriptor descriptorFromDtd = getNSDescriptorFromMetaData(
        xmlFile == null ? null : AstLoadingFilter.forceAllowTreeLoading(xmlFile, xmlFile::getDocument), forHtml);

      LOG.debug("Descriptor from meta data for schema file " +
                schemaFilePath +
                " is " +
                (descriptorFromDtd != null ? descriptorFromDtd.getClass().getCanonicalName() : "NULL"));

      if (descriptor != null && descriptorFromDtd != null){
        descriptor = new XmlNSDescriptorSequence(new XmlNSDescriptor[]{descriptor, descriptorFromDtd});
      }
      else if (descriptorFromDtd != null) {
        descriptor = descriptorFromDtd;
      }
    }
    return descriptor;
  }

  @Nullable
  private XmlNSDescriptor getNSDescriptorFromMetaData(@Nullable PsiMetaOwner metaOwner, boolean nonEmpty) {
    if (metaOwner == null) return null;
    XmlNSDescriptor descriptor = (XmlNSDescriptor)metaOwner.getMetaData();
    if (descriptor == null) return null;
    if (nonEmpty && descriptor.getRootElementsDescriptors(this).length == 0) {
      return null;
    }
    return descriptor;
  }

  @NotNull
  @Override
  public CompositePsiElement clone() {
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict = new HashMap<>(
      myDefaultDescriptorsCacheStrict
    );
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict = new HashMap<>(
      myDefaultDescriptorsCacheNotStrict
    );
    final XmlDocumentImpl copy = (XmlDocumentImpl) super.clone();
    updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict);
    return copy;
  }

  @Override
  public PsiElement copy() {
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheStrict = new HashMap<>(
      myDefaultDescriptorsCacheStrict
    );
    HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict = new HashMap<>(
      myDefaultDescriptorsCacheNotStrict
    );
    final XmlDocumentImpl copy = (XmlDocumentImpl)super.copy();
    updateSelfDependentDtdDescriptors(copy, cacheStrict, cacheNotStrict);
    return copy;
  }

  private void updateSelfDependentDtdDescriptors(XmlDocumentImpl copy, HashMap<String,
    CachedValue<XmlNSDescriptor>> cacheStrict, HashMap<String, CachedValue<XmlNSDescriptor>> cacheNotStrict) {
    copy.myDefaultDescriptorsCacheNotStrict = ContainerUtil.newConcurrentMap();
    copy.myDefaultDescriptorsCacheStrict = ContainerUtil.newConcurrentMap();

    for(Map.Entry<String, CachedValue<XmlNSDescriptor>> e:cacheStrict.entrySet()) {
      if (e.getValue().hasUpToDateValue()) {
        final XmlNSDescriptor nsDescriptor = e.getValue().getValue();
        if (!isGeneratedFromDtd(nsDescriptor)) copy.myDefaultDescriptorsCacheStrict.put(e.getKey(), e.getValue());
      }
    }

    for(Map.Entry<String, CachedValue<XmlNSDescriptor>> e:cacheNotStrict.entrySet()) {
      if (e.getValue().hasUpToDateValue()) {
        final XmlNSDescriptor nsDescriptor = e.getValue().getValue();
        if (!isGeneratedFromDtd(nsDescriptor)) copy.myDefaultDescriptorsCacheNotStrict.put(e.getKey(), e.getValue());
      }
    }
  }

  @Override
  public PsiMetaData getMetaData() {
    return MetaRegistry.getMeta(this);
  }

}
